总结
- 虽然现代主流浏览器已支持 ESM,但在部分老旧的浏览器仍不支持,为了向下兼容,webpack 仍然会将
ESM
转化为CommonJS
,并注入到运行时代码。
其实代码实现上,也不是转换成CJS
,而是转换为浏览器可以识别的更早规范ES5
(语言层级的规范,浏览器支持程度很高,不支持的都基本被淘汰了)。只是代码实现形式上很贴合CJS
转ES5
的实现,在CJS
转换ES5
的基础上添加代码去实现ESM
的特性。 __webpack_require__.r
标记其为 ESM 模块 (暂时没找到 r 的单词)__webpack_require__.d
定义其所有导出的值 (define)__webpack_require__.o
判断是否实例的属性 (ownProperty)__webpack_require__
与CJS
的运行时一致,意味着执行的传参和CJS
的运行时代码一致,依旧还是 module、module.exports、require。且模块返回还是module.exports
。- 执行的传参和
CJS
的运行时代码一致,只不过CJS
是为module.exports
添加数据属性或直接赋值为堆里的另一个引用地址。而ESM
是为其添加访问器属性 getter。 module_wrapper
包裹函数内,对module.exports
使用__webpack_require__.r
进行标记,表示该模块已经处理过,且使用__webpack_require__.d
定义其所有导出的访问器属性
提问
[x] 对含 ESM 模块的 webpack 运行时代码进行调试与理解
[x] webpack 含 ESM 的运行时代码做了那些事情
主体框架与CJS
运行时代码一致。- 利用 AST 生成所有模块数组
__webpack_modules__
,内部都是包裹函数。 - 生成缓存对象
__webpack_module_cache__
- 实现模块加载器
__webpack_require__
,可以利用__webpack_module_cache__
读取缓存,或直接从所有模块数组__webpack_modules__
中取得模块执行并缓存 - 入口模块执行,即 0 号模块执行。
区别在于,模块加载器
__webpack_require__
函数扩展了以下三个方法:(在函数上扩展属性,目的是为了封装,且避免和模块内变量重名)__webpack_require__.r
标记其为 ESM 模块__webpack_require__.d
定义其所有导出的值 (define)__webpack_require__.o
判断是否实例的属性 (ownProperty)
且每个模块,即每个包裹函数内,对
module.exports
使用__webpack_require__.r
进行标记该模块已经处理过,且使用__webpack_require__.d
定义其所有导出的访问器属性- 利用 AST 生成所有模块数组
[x] webpack_require 中的 d/r/o 各个变量指什么意思
__webpack_require__.r
标记其为 ESM 模块__webpack_require__.d
定义其所有导出的值 (define)__webpack_require__.o
判断是否实例的属性 (ownProperty)
1. 前提提要、场景
虽然现代主流浏览器已支持 ESM,但在部分老旧的浏览器仍不支持,为了向下兼容,webpack 仍然会将 ESM
转化为 CommonJS
,并注入到运行时代码。
其实代码实现上,也不是转换成 CJS
,而是转换为浏览器可以识别的更早规范 ES5
(语言层级的规范,浏览器支持程度很高,不支持的都基本被淘汰了)。只是代码实现形式上很贴合 CJS
转 ES5
的实现,在 CJS
转换 ES5
的基础上添加代码去实现 ESM
的特性。
webpack
是如何将 ESM
转化为 CommonJS
的,学习这个转换过程,有利于理解 ESM
的特性及其实现。
2. 导出转换
ESM
中,有 命名导出 和 默认导出 两种的方式,但在 CJS
中只有一种。
所以需要进行转换
default
转化为module.exports.default
name
转化为module.exports.name
js
// esm 代码
import sum, { name } from './sum'
import * as s from './sum'
// 转化后的 cjs 代码
const s = require('./sum')
const sum = s.default
const name = s.name
3. 运行时代码分析
js
/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony exports
// 定义 get 函数,实现 ESM 属性的只读引用,即属性懒加载
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
// 判断是否实例的属性 且 未在exports中定义过
// 目的:只使用 definition 实例的属性,为 exports 设置 getter,且不重复设置。
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
/* webpack/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
/* webpack/runtime/make namespace object */
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
// 支持 Symbol,也设置 Symbol.toStringTa, Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
// 设置 exports 的 __esModule 字段为 true,即标记为 ESM 模块
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
3.1 webpack_require.r 标记是一个 ESM 模块
暂时不知道 r 是哪个单词的缩写。
js
/* webpack/runtime/make namespace object */
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
// 支持 Symbol,也设置 Symbol.toStringTa, Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
// 设置 exports 的 __esModule 字段为 true,即标记为 ESM 模块
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
3.2 webpack_require.d 定义 get 函数(define)
js
/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony exports
// 定义 get 函数,实现 ESM 属性的只读引用,即属性懒加载
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
// 判断是否实例的属性 且 未在exports中定义过
// 目的:只使用 definition 实例的属性,为 exports 设置 getter,且不重复设置。
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
3.3 webpack_require.o 判断是否实例的属性(OwnProperty)
js
/* webpack/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
3.4 Symbol.toStringTag 设定 Object.prototype.toString 返回值
Object.prototype.toString()
方法会去读取 Symbol.toStringTag
并把它包含在自己的返回值里
js
var t = {}
Object.prototype.toString.call(t) // '[object Object]'
Object.defineProperty(t, Symbol.toStringTag, { value: 'Module' });
Object.prototype.toString.call(t) // '[object Module]'
3.5 运行时代码分析
sum.js
js
// 源码
const sum = (...args) => args.reduce((x, y) => x + y, 0)
export default sum
export const name = 'sum'
// 运行时代码
// 该模块被 module_wrapper 包裹,依旧还是 module、module.exports、require
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// 1. 标记该模块是一个 ESM 模块
__webpack_require__.r(__webpack_exports__);
// 2. 导出所有的属性,即 __webpack_exports__,但通过 getter/setter 方式,可以懒加载属性
__webpack_require__.d(__webpack_exports__, {
"default": () => (__WEBPACK_DEFAULT_EXPORT__),
"name": () => (/* binding */ name)
});
// 3. 执行代码,配置 getter/setter 的属性
const sum = (...args) => args.reduce((x, y) => x + y, 0)
// 即 export default
const __WEBPACK_DEFAULT_EXPORT__ = (sum);
const name = 'sum'
})
index.js
js
// 源码
import sum, { name } from './sum'
import * as s from './sum'
console.log(sum(3, 4))
console.log(name)
console.log(s)
// 运行时代码
(() => { // webpackBootstrap
"use strict";
var __webpack_modules__ = ([
/* 0 */,
/* 1 */((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"default": () => (__WEBPACK_DEFAULT_EXPORT__),
"name": () => (/* binding */ name)
});
const sum = (...args) => args.reduce((x, y) => x + y, 0)
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (sum);
const name = 'sum'
})
]);
/************************************************************************/
// 缓存都和 CJS 的运行时代码一致
// The module cache
var __webpack_module_cache__ = {};
// The require function
// require 与 CJS 的运行时一致
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
// 执行的传参和 CJS 的运行时代码一致,依旧还是 module、module.exports、require
// 执行的传参和 `CJS` 的运行时代码一致,依旧还是 module、module.exports、require。只不过 `CJS` 是为 `module.exports` 添加数据属性或直接整个改写赋值。而 `ESM` 是为其添加访问器属性 getter。
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports; // 还是返回模块的 module.exports
}
/************************************************************************/
/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony exports
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
/* webpack/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
/* webpack/runtime/make namespace object */
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
__webpack_require__.r(__webpack_exports__);
var _sum__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); // 得到模块的 module.exports
console.log((0,_sum__WEBPACK_IMPORTED_MODULE_0__["default"])(3, 4)) // 取得模块的访问器属性 default
console.log(_sum__WEBPACK_IMPORTED_MODULE_0__.name) // 取得模块的访问器属性 name
console.log(_sum__WEBPACK_IMPORTED_MODULE_0__)
})();
/******/ })()
;
__webpack_require__
与CJS
的运行时一致,意味着执行的传参和CJS
的运行时代码一致,依旧还是 module、module.exports、require。且模块返回还是module.exports
。- 执行的传参和
CJS
的运行时代码一致,只不过CJS
是为module.exports
添加数据属性或直接整个改写赋值。而ESM
是为其添加访问器属性 getter。
疑问
- [ ]